#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         CountryEntitySource.py
#
# Copyright:    Copyright (c) 2006, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  An 'EntitySource' that provides Model assessments as
#               entities.
#
# See Also:     Entity, *EntityChooser, *EntitySource
#####################################################################

#####################################################################
## Zope Library Imports
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.Role import RoleManager
 
#####################################################################
## Python Library Imports
# (none)
#####################################################################
## Knowledge Engine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Entity import Entity, DummyEntity
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.EventDefinition import *
from Products.KnowledgeEngine.PermissionDefinition import *
from Products.KnowledgeEngine.EntitySource import EntitySource, EntitySourceInfo

#####################################################################
## CMF Library Imports
from Products.CMFCore import permissions
from Products.CMFCore.ActionInformation import ActionInformation
from Products.CMFCore.utils import getToolByName

#####################################################################
## Zope Library Imports
from Globals import DTMLFile
from Globals import Persistent
from App.ImageFile import ImageFile
from Products.ZSQLMethods.SQL import SQL
from DateTime import DateTime
from AccessControl import ClassSecurityInfo

#####################################################################
## Python Library Imports
import os
import base64
import binascii

def getModels(context):
    """
        Return the models in the portal as [(Title,Identity)]
    """
    
    #get the portal catalog
    catalog = getToolByName(context, "knowledgeengine_genericobjectcatalogservice")
    
    #search for entity sources by their family
    searchResults = catalog(getClassName = "Model")
    
    #results aren't the actual objects, so lets reference them
    result = []
    for searchResult in searchResults:
        #attempt to resolve the actual object reference
        model = searchResult.getObject()
        
        #only add models that truely exist!
        if model is not None:
            result.append((model.getTitleOrId(), model.getIdentity()))
    
    return result


#####################################################################
## 
def getModelRepositories(context):
    """
        Returns a list of the repositories in the selected model
    """

    modelIdentity = context.getPropertyValue('sourceModelIdentity')
    if not modelIdentity:
        models = context.getModelsByIdentity()
    else:
        models = context.getModelsByIdentity([modelIdentity, ])
    
    result = []
    if len(models) > 0:
        model = models[0]
        result.append(("(All Repositories)", "_ALL"))
        result.append(("(Users Personal Repository)", model.getIdentity()))
        result.extend(map(lambda r: (r.getTitleOrId(), r.getIdentity()), model.getRepositories()))

    return result


#####################################################################
## ModelEntitySourceInfo Class Definition
class ModelEntity(Entity):
    """
        A model entity i.e. assessment
    """

    #---------------------------------------------------------------#
    # Declare security so ClassInfo may be accessed in DTML/ZPT etc
    security = ClassSecurityInfo()
    security.declareObjectPublic()
    security.setDefaultAccess("allow")

    def __getattr__(self, name):
        """ Allow entity.componentIdentity in templates etc. """
        
        # Data for a model is an assessment. Assume the 'name' is
        # a component identity and return it's value
        
        print "Getattr: %s from %s ( %s )" % ( name, self._data.getIdentity(), self._data.getModel().getIdentity() )
        
        value = self._data.getComponentByIdentityValue(name)
        if value is not None:
            return value
        
        raise AttributeError, name


#####################################################################
## ModelEntitySourceInfo Class Definition
class ModelEntitySourceInfo(EntitySourceInfo):
    """
        Defines the class info methods, defining information about
        the class without needing an instance of the class
    """

    #---------------------------------------------------------------#

    def getClassName(self):
        """
            Returns the name of the class for which
            we are providing information.
        """
        return 'ModelEntitySource'

    #---------------------------------------------------------------#

    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """

        return "ModelEntitySource"

    #---------------------------------------------------------------#

    def getDescription(self):
        """
            ABSTRACT: Return a friendly description on the purpose
            of this class.  This description is often used in the
            Property Editor for instances of this class
        """

        return "Defines a Model as a source for Entities"

    #---------------------------------------------------------------#

    def getIdentityType(self):
        """
            Return the identity type.
        """

        return "esModel"

    #---------------------------------------------------------------#

    def getClassIcon(self):
        """
            Return the appropriate icon file for the class.
        """

        return ImageFile('skins/ModelEntitySource_icon.gif', globals())

    #---------------------------------------------------------------#

    def isContainer(self):
        """
            Returns if the instance is providing folderish/container
            services.

            Override an return true if the instance is to be
            a container for other instances.

            NOTE: all instances support "events" so there is no
            need to return true if you require "event" support.
        """

        return false

    #---------------------------------------------------------------#

    def isPermittedInContainer(self, containerClassInfo, containerInstance = None, instance=None):
        """
            ABSTRACT: Returns if class instances (instance) is permitted in
            the specified container (containerInstance)

            NOTE: This is from this class' instance perspective.
        """

        #ModelEntitySources don't contain anything
        return false

    #---------------------------------------------------------------#

    def isChildPermitted(self, childClassInfo, childInstance = None):
        """
            Returns if the specified child is permitted as a
            child of the class' instances.

            NOTE: This is from this class' instance perspective.

            For component containers, this always overrides what the
            return value from childClassInfo.isPermittedInContainer(...)
        """

        #ModelEntitySources don't contain anything
        return false

    #---------------------------------------------------------------#

    def isPortalObject(self):
        """
            Returns if the object can be placed directly in 
            a Plone Portal (rather than like most objects
            that must exist within a GenericObject container
            object.
            
            eg: Models, EntitySources are Portal Objects.
        """
        
        return true

    #---------------------------------------------------------------#

    def onConfigureProperties(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register
            class specific properties using the PropertyReflection interface.

            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered

            NOTE: This method is called before ClassInfo.onAfterStartup()
        """

        #initialise the super-class properties for reflection
        EntitySourceInfo.onConfigureProperties(self)

        #add the 'sourceModelIdentity' property        
        self.addPropertyDefinition(
               PropertyDefinition(identity="sourceModelIdentity", 
                                  displayName="Source Model", 
                                  description="The Model that will provide assessments as entities", 
                                  propertyType=pt_STRING,
                                  propertyFormat=pf_NATIVE,
                                  defaultValue=None,
                                  hasEnumeratedValues=true,
                                  enumeratedValuesFunction=getModels,
                                  constructorEVAP=evap_READWRITE,
                                  propertyEditorEVAP=evap_READONLY,
                                  isMandatory=true))        

        self.addPropertyDefinition(
            PropertyDefinition(
                identity="modelRepositories",
                displayName="Model Repositories",
                description="Assessments will only be fetched from the selected model repositories selected. Permissions will be respected.",
                defaultValue=[],
                propertyType=pt_NATIVE,
                propertyStructure=ps_SET,
                propertyFormat=pf_NATIVE,
                hasEnumeratedValues=true,
                enumeratedValuesFunction=getModelRepositories,
                constructorEVAP=evap_READWRITE,
                propertyEditorEVAP=evap_READWRITE
            )
        )

        #add the 'entityNameSingular' property        
        self.addPropertyDefinition(
               PropertyDefinition(identity="entityNameSingular", 
                                  displayName="Singular Terminology", 
                                  description="What does a single Assessment in the Source Model represent? (eg: Patient, Product, Assessment)", 
                                  propertyType=pt_STRING, 
                                  propertyFormat=pf_TEXT_SINGLELINE, 
                                  defaultValue="Assessment",
                                  constructorEVAP=evap_READWRITE, 
                                  propertyEditorEVAP=evap_READWRITE,
                                  isMandatory=true))   

        #add the 'entityNamePlural' property        
        self.addPropertyDefinition(
               PropertyDefinition(identity="entityNamePlural", 
                                  displayName="Plural Terminology", 
                                  description="The name for a collection of assessments from the Source Model  (eg: Patients, Products, Assessments)", 
                                  propertyType=pt_STRING, 
                                  propertyFormat=pf_TEXT_SINGLELINE, 
                                  defaultValue="Assessments",
                                  constructorEVAP=evap_READWRITE, 
                                  propertyEditorEVAP=evap_READWRITE,
                                  isMandatory=true))
                                  
        #update the title property documentation to include that leaving it blank
        #will force the component to use the underlying Source Model title.
        self.getPropertyDefinition("title").setDescription(self.getPropertyDefinition("title").getDescription() + " (leave blank to use the Source Model title)")

    #---------------------------------------------------------------#

    def onConfigureEvents(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register
            class specific events using the EventReflection interface.

            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered

            NOTE: This method is called before ClassInfo.onAfterStartup()
        """

        #initialise the super-class events for reflection
        EntitySourceInfo.onConfigureEvents(self)

    #---------------------------------------------------------------#

    def onConfigurePermissions(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register
            class specific permission definitions.

            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered

            NOTE: This method is called before ClassInfo.onAfterStartup()
        """

        #initialise the super-class permissions for reflection
        EntitySourceInfo.onConfigurePermissions(self)

    #---------------------------------------------------------------#

#####################################################################
## ModelEntitySource Class Definition
class ModelEntitySource(EntitySource):
    """
        Model Entity Source
    """

    security = ClassSecurityInfo()

    #---------------------------------------------------------------#

    #ABSTRACT TRANSIENT: a ClassInfo implementation instance to provide info about this class
    _CLASSINFO = ModelEntitySourceInfo()

    #---------------------------------------------------------------#
    # Default Attribute Values
    meta_type = 'ModelEntitySource'

    #---------------------------------------------------------------#

    def __init__(self, id, identity):
        "Constructor for the object"

        EntitySource.__init__(self, id, identity)

        #perform onAfterInstanciation
        self.onAfterInstanciation()

    #---------------------------------------------------------------#

    def onAfterInstanciation(self):
        """
            Handle Knowledge Engine specific actions after this object
            has been loaded into memory.

            This is useful for performing backwards compatibility
        """

        #perform default onAfterInstanciation for the container
        EntitySource.onAfterInstanciation(self)

    #---------------------------------------------------------------#

# 20061008 johnm    sourceModelIdentity not available on EntitySource
#                   init.
#
# TODO: Need to clean this up
#
#    def getTitle(self):
#        """
#            Returns the Title of the Model Source itself
#            if the title is blank (and the Model Source is configured)
#        """
#        
#        if len(self.title) > 0:
#            return self.title
#        elif self.getSourceModelIdentity() is not None:
#            return self.getSourceModel().getTitleOrId()
#        else:
#            return EntitySource.getTitle(self)

    #---------------------------------------------------------------#

    def getSourceModelIdentity(self):
        """
            Return the identity of the Model that is providing
            assessments as entities
        """
        
        if hasattr(self, 'sourceModelIdentity'):
            return self.sourceModelIdentity
        
        return None
    
    #---------------------------------------------------------------#
    
    def setSourceModelIdentity(self, value):
        """ Sets the source model identity """
        
        setattr(self, 'sourceModelIdentity', value)
    
    #---------------------------------------------------------------#

    def getModelRepositories(self):
        """
            Return the repositories for the selected model
        """
        if hasattr(self, 'modelRepositories'):
            return self.modelRepositories
        
        return None
    
    #---------------------------------------------------------------#

    def getSourceModel(self):
        """
            Return the Model that is providing the Entities
        """
    
        #get the portal catalog
        catalog = getToolByName(self, "knowledgeengine_genericobjectcatalogservice")
        
        #search for model based on it's identity
        searchResults = catalog(getIdentity = self.getSourceModelIdentity())
        
        #could we find the object?
        if len(searchResults) > 0:
            return searchResults[0].getObject()
        else:
            return None

    #---------------------------------------------------------------#

    def _fetchAssessments(self, user = None, batchNr = None, batchSize = 30, sortBy = None, filterBy = {}):
        """
            Fetches the 
        """
        
        # Required services
        assessmentStorageService    = getAssessmentStorageService(self)
        securityService             = getSecurityService(self)
        userProviderService         = getUserProviderService(self)
        
        # Authenticated user is required for the ModelIdentitySource. If a user
        # has not been given, try and grab it from the filter, or the currently authenticated user.
        if user is None:
            #Issue #928 - Dont use REQUEST.AUTHENTICATED_USER - Zope seems to randomly stop adding it to the request object?
            user = userProviderService.getUser(filterBy.pop('authenticatedUser', self.portal_membership.getAuthenticatedMember().getId()))
            
            
            
        
        # Iterate through the repositories selected, making sure the authenticated user
        # does in fact have access to both the model and the specified repository
        assessments = []
        model = self.getSourceModel()
        
        if not model:
            print "ModelEntitySource::_fetchAssessments() - Cannot find source model"
            return []
        
        if model.isAccessibleByUser(user):
                       
            # First, check ListAsssessments permission on the model. If set, we have access to all repositories
            modelAccessGranted = securityService.granted( 'ListAssessments', model.getIdentity(), user.getIdentity() )
                
                
                
            for repositoryIdentity in self.getModelRepositories():
                
                repositoryAccessGranted = modelAccessGranted
                
                if repositoryIdentity == '_ALL':
                    
                    
                    # Only allow '_ALL' if ListAssessments permiss
                    if not modelAccessGranted: continue
                                       
                    # We are getting all assessments in the model
                    assessments.extend(assessmentStorageService.getAssessmentsForModel(model, batchNr = batchNr, batchSize = batchSize, sortBy = sortBy))
                
                # If we don't have access from the model, try and check acces for each repository
                elif not repositoryAccessGranted:

                    # We have to make an exception for the personal repository as it's named after the user identity
                    if repositoryIdentity == model.getIdentity():
                        repositoryIdentity = user.getIdentity()
                    
                        ## Ensure permissions to personal repository is granted to the authenticated user
                        if securityService.granted( 'AccessPersonalRepository', model.getIdentity(), user.getIdentity() ):
                            repositoryAccessGranted = True                     
                                                                
                    else:
                        if securityService.granted( 'AccessRepository', repositoryIdentity, user.getIdentity()) \
                        or securityService.granted( 'ListAssessments',  repositoryIdentity, user.getIdentity()):
                            repositoryAccessGranted = True
                    
                
                # We have access to the repository, add its assessments.
                if repositoryAccessGranted:   
                    
                    # Make sure the authenticated user has access to the specific repository
                    assessments.extend(assessmentStorageService.getAssessmentsInRepository(
                        model, repositoryIdentity, batchNr = batchNr, batchSize = batchSize, sortBy = sortBy
                    ))

        # Determine the number of assessments for the source model. We need
        # to make sure the assessments contain data otherwise confusion
        # ensues
        results = []
        for assessment in assessments:
            if assessmentStorageService.assessmentDataExists(assessment.getIdentity()):
                results.append(assessment)

        # Now return only those assessments which satisfy the filter
        assessments = results
        results = []
                
        if filterBy:
            for assessment in assessments:
                for (componentIdentity, value) in filterBy.items():
                    # NOTE: Equality may be something we're not looking for
                    #       here. Should probably investigate a solution
                    #       involving KERL or similar (johnm)
                    
                    componentValue = assessment.getComponentByIdentityValue(componentIdentity)                    
                    if componentValue:
                        if value.lower() in componentValue.lower():
                            results.append(assessment)
                        
        else:
            results = assessments                        

        return results

    #---------------------------------------------------------------#

    def getNrEntities(self, user = None, filterBy = {}):
        """
            Returns the number of entities available from this
            ModelEntitySource for the specified user.
            
            The user (optional) is provided to enable ModelEntitySources to filter by user.

            TODO: Implement filtering
        """

        return len( self.getEntities(), user=user, filterBy=filterBy )

        #assessments = self._fetchAssessments(user = user, filterBy = filterBy)

        #return len(assessments)

    #---------------------------------------------------------------#

    def getEntities(self, user = None, filterBy = {}):
        """
            Returns the entities from this entity source,
            ordered by entity identity.
            
            TODO: Implement filtering
        """

        assessments = self._fetchAssessments(user = user, filterBy = filterBy)

        return self._filterEntities( [ModelEntity(assessment.getIdentity(), assessment.getName(), data = assessment) for assessment in assessments] )
        

    #---------------------------------------------------------------#

    def getEntitiesByBatch(self, batchNr, batchSize = 10, user = None, filterBy = {}):
        """
            Returns a batch of entities as specified.
            
            NOTE 1: The first available batch is 0
            
            NOTE 2: The entities are returned in order of their name
            
            TODO: Implement filtering
        """
        
        assessments = self._fetchAssessments(user = user, batchNr = batchNr, batchSize = batchSize, filterBy = filterBy)

        return self._filterEntities( [ModelEntity(assessment.getIdentity(), assessment.getName(), data = assessment) for assessment in assessments] )

    #---------------------------------------------------------------#

    def getEntity(self, entityIdentity, user = None):
        """
            Returns the entity with the specified identity
            for the provided user
        """

        #has a real entityIdentity been requested?
        entity = None
        if entityIdentity is not None:                
            #get the assessment storage service we're currently using
            storageService = getAssessmentStorageService(self)

            #attempt to load the assessment
            try:
                assessment = storageService.loadAssessment(entityIdentity)
                if assessment is not None:
                    entity = ModelEntity(assessment.getIdentity(), assessment.getName(), data = assessment)
                    if assessment.isOrphaned():
                        entity.setTitle("[Orphaned] %s" % entity.getTitle())
            except:
                # Use a dumy entity instead of None or failing
                entity = DummyEntity(entityIdentity, 'INVALID')

        return entity            
        
    #---------------------------------------------------------------#

    def getEntityNameSingular(self):
        """
            ABSTRACT: Return the name of a single entity provided 
            by the Entity Source
        """
        
        return self.entityNameSingular
        
    #---------------------------------------------------------------#

    def getEntityNamePlural(self):
        """
            ABSTRACT: Return the name of a collection of entities provided 
            by the Entity Source
        """
        
        return self.entityNamePlural


#####################################################################
## Class Initialisation

#####################################################################
## CMF Required Class Initialisation

def addModelEntitySource(self, id, REQUEST=None):
    """
        The Zope/CMF factory method to create an instance
    """

    #create a KnowledgeEngine identity for the new model
    identity = generateIdentity(ModelEntitySource.identity_type)

    #create an instance
    object = ModelEntitySource(id, identity)

    #add the object to the specified container (called self)
    self._setObject(id, object)

    if REQUEST is not None:
        REQUEST['RESPONSE'].redirect(object.absolute_url() + '/manage_main' )


#the CMF/Plone factory type information - used to create objects!)
factoryTypeInformation = {
                               'id'             : 'ModelEntitySource'
                             , 'meta_type'      : 'ModelEntitySource'
                             , 'description'    : "A KnowledgeEngine ModelEntitySource"
                             , 'icon'           : 'ModelEntitySource_icon.gif'
                             , 'product'        : 'KnowledgeEngine'
                             , 'factory'        : 'addModelEntitySource'
                             , 'filter_content_types' : 0
                             , 'global_allow'   : 0
                             , 'immediate_view' : 'view'
                             , 'actions'        :
                                ( { 'id'            : 'view'
                                  , 'name'          : 'View'
                                  , 'action'        : 'string:${folder_url}/view'
                                  , 'permissions'   : (permissions.View,)
                                  , 'category'      : 'folder'
                                  },
                                )
                         }

#####################################################################
## Class Initialisation

# Register Class Information with the KnowledgeEngine
registerPortalObjectClass(ModelEntitySource, addModelEntitySource, factoryTypeInformation)

#####################################################################
